#!/usr/bin/env python

import httplib,urllib2,urllib
import os
from sgmllib import SGMLParser
import subprocess
import ConfigParser
import re
import sys
import socket
import time
from datetime import datetime as dt
import mimetypes
import tempfile 

debug = False
CONF_FILE="brender.conf"


tmp_dir=tempfile.mkdtemp(prefix='brender',dir='/tmp/') + '/'

if  len(sys.argv) > 1 and sys.argv[1] == "-d":
  debug=True
else:
  debug=False

VERSION=0.1
DATE="$Date$"
REV="$Revision$"
VERSION_TXT=str(VERSION) + '_r' + re.search('[\d]+',str(REV)).group(0) 
BRENDER_AGENT="Brender Client v%s" % VERSION_TXT

## Configurable site info
site_info = {}
site_info['host']          = "www.sprocketandlube.com"
site_info['port']          = 80
site_info['url']           = "http://www.sprocketandlube.com"
site_info['base_dir']      = "/brender_dev/"
site_info['frame_page']    = "gimiaframe.php"
site_info['register_page'] = "addclient.php"
site_info['upload_page']   = "upload_finished_frame.php"
site_info['heartbeat_page'] = "update_frame_progress.php"

class BNetwork(object):

    def __init__(self,host,post_data,port=80,multipart=False):
        self.host = host
        self.port = port
        self.multipart = multipart
        self.post_data = post_data

    def post_to_url(self):
        if self.multipart is True:
            content_type, body = self.encode_multipart_formdata()
        else:
            content_type = "application/x-www-form-urlencoded"
            body = urllib.urlencode(self.post_data['fields'])


        headers = {
            'User-Agent': BRENDER_AGENT,
            'Content-Type': content_type,
            'Accept': 'text/plain',
            }

        data = None
        while data is None:
            try:
                conn = httplib.HTTPConnection(self.host,port=self.port)
                conn.request("POST",self.post_data['page'],body,headers)
                response=conn.getresponse()
                data = response.read()
                conn.close()
            except:
                data = None
                print "Caught Network exceptions.. retrying in 10 seconds"
                time.sleep(10)

        return data

    def post_multipart(host, selector, fields, files):
        try:
            h = httplib.HTTPConnection(host)  
            h.request('POST', selector, body, headers)
            res = h.getresponse()
        except:
            print "Post multipart exception"
            raise
        return res.read()

    def encode_multipart_formdata(self):
        fields = self.post_data['fields']
        files = self.post_data['files']

        """
        fields is a sequence of (name, value) elements for regular form fields.
        files is a sequence of (name, filename, value) elements for data to be uploaded as files
        Return (content_type, body) ready for httplib.HTTP instance
        """
        BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$'
        CRLF = '\r\n'
        L = []
        for (key, value) in fields:
            L.append('--' + BOUNDARY)
            L.append('Content-Disposition: form-data; name="%s"' % key)
            L.append('')
            L.append(value)
        for (key, filename, value) in files:
            L.append('--' + BOUNDARY)
            L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename))
            L.append('Content-Type: %s' % self.get_content_type(filename))
            L.append('')
            L.append(value)
        L.append('--' + BOUNDARY + '--')
        L.append('')
        body = CRLF.join(L)
        content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
        return content_type, body

    def get_content_type(self,filename):
        return mimetypes.guess_type(filename)[0] or 'application/octet-stream'

# class to parse the sgml
class BParser(SGMLParser):
    def reset(self):                        
        SGMLParser.reset(self)
        self.data = {}
        self.process=0
        
    def _process(self,attrs):
        self.process=1

        if debug is True:
            print "DEBUG: processing... %s " % self.lasttag
        
    def do_response(self,attrs):
        self._process(attrs)
        
    def do_response_message(self,attrs):
        self._process(attrs)
   
    def do_blender_file_url(self,attrs):
        self._process(attrs)
        
    def do_frame_to_render(self,attrs):
        self._process(attrs)
    
    def do_job_id(self,attrs):
        self._process(attrs)

    def do_client_id(self,attrs):
        self._process(attrs)
    
    def do_file_hash(self,attrs):
        self._process(attrs)
    
    def handle_data(self,text):
        if self.process == 1:
            self.data[self.lasttag] = text
            
            if debug is True:
                debug_msg(text)

            self.process = 0



def cleanup():

    print "Cleaning up tmp dir"
    os.removedirs(tmp_dir)

def send_heartbeat(job,seconds,progress_percent):
    host = site_info['host'] 
    clientid = job['client_id']
    postdata = {}
    postdata['page'] = site_info['base_dir'] + site_info['heartbeat_page']
    postdata['fields'] = {'client_id':clientid,'render_time':seconds,'progress':progress_percent}

    if debug is True:
        debug_msg("send_heartbeat: postdata: %s " % postdata)

    data = BNetwork(host,postdata).post_to_url()

    parser = BParser()
    parser.feed(data)
    parser.close

    res = parser.data

    if debug is True:
        debug_msg("send_heartbat: response: %s " % res )

    if res.has_key('response'):
        if res['response'] == 'Abort Frame':
            return False
        elif res['response'] == 'Ok':
            return True
    else:
        return True

# Grab the blend file.
def grab_blend_file(job,tmpdir):
    hashid  = job['file_hash']
    url     = job['blender_file_url']
    jobid   = job['job_id']

    count=0
    data=''
    byte=1
    
    filename  = "%s%s_%s.blend" % ( tmpdir,hashid, jobid) 

    if os.path.exists(tmpdir):
		pass
    else:
		os.mkdir(tmpdir)

    if os.path.exists(filename):
       if debug is True:
         print "DEBUG: filename: %s" % filename
       print "\tNo need to grab this blend file we already have it!"
    else: 
       sock = urllib.urlopen(url)
       print "\tDownloading the blend : %s" % url
       
       while byte is not '':
           byte = sock.read(1)
           data += byte
           count += 1
       
       print "\tdownloaded %d bytes" % count

       sock.close()

       f = open(filename,'wb')
       f.write(data)
       f.close()
    return filename

def register_client(name):

    postdata = {}
    postdata['page'] = site_info['base_dir'] + site_info['register_page']
    postdata['fields'] = {'friendly_name':name}
    
    print "Using client name %s" % str(name)
     
    data = BNetwork(site_info['host'],postdata).post_to_url()

    parser = BParser()
    parser.feed(data)
    parser.close()

    
    if debug is True:
        print "DEBUG: %s " %data
        print "DEBUG: parsed data - %s" % parser.data

    return parser.data


def upload_image(frame_path,job):
    postdata = {} 
    postdata['page'] = site_info['base_dir'] + site_info['upload_page']

    file_data = open(frame_path,'rb').read()

    print "\tUploading completed frame '%s' (%d bytes)..." % (frame_path,len(file_data))
    postdata['fields'] = [('client_id',job['client_id']),
                         ('frame_number',job['frame_to_render']),
                         ('job_id',job['job_id']),
                        ]
    
    postdata['files'] = [('finished_frame',frame_path,file_data),]

    if debug is True:
        debug_msg("upload_image: postdata: %s " % postdata)
    
    rdata = BNetwork(site_info['host'],postdata,multipart=True).post_to_url()
    
    if debug is True:
       debug_msg("upload_image: rdata %s" % rdata)
    

def debug_msg(text):
    print "DEBUG:  %s" % text

#render me a frame
def render_frame(conf_dict,blend_file,job,tmpdir):

    blender_bin = conf_dict['blender_bin']
    cont = True
    render_dir = tmpdir + 'render/'

    if os.path.exists(render_dir):
        pass
    else:
        os.mkdir(render_dir)

    frame = job['frame_to_render']
    clientid = job['client_id']
    jobid = job['job_id']

    cmd = "%s" % blender_bin
    args = "-b %s -o %s -F PNG -f %s  -x 1" % ( blend_file,
                                               render_dir ,
                                               frame)
    command = cmd + ' ' + args

    frame_location_regex = re.compile("^Saved: ([\w\W]+) Time:")
    

    if debug is True:
        if os.name == 'nt': 
           debug_msg("executing this command: %s" % (cmd + args))
        else:
           debug_msg("executing this command: %s" % command)

    print "Starting to render...." , dt.now()
    
    if debug is True:
        debug_msg("communicating with blender subprocess..waiting for render to finish")
    render_start=time.time() 

    if os.name == 'nt':
        blender_proc = subprocess.Popen(command, stdout=subprocess.PIPE)
    else:
        blender_proc = subprocess.Popen('exec ' + command, shell=True,stdout=subprocess.PIPE)

    err=0
    try:
        while  blender_proc.poll() is None:
            # Wait X seconds before sending update
            if debug is True:
                debug_msg("blender pid: %d " % int(blender_proc.pid))
                debug_msg("sleeping...")

                for i in range(0,60):
                    time.sleep(1)
                    if blender_proc.poll() is not None:
                        break

            # get how many seconds we've spent on this so far
            if cont is True:
                duration = time.time() - render_start

            if cont is True:
                # Update the server with the heartbeat
                if debug is True:
                    debug_msg("sending heartbeat: %.2f ..." % duration)

                cont = send_heartbeat(job,duration,0)

            if cont is False:
                print "\tAborting rendering this frame, server said so."
                print "\tkilling process %s" % blender_proc.pid
                if os.name == 'nt':
                    os.popen('TASKKILL /PID '+str(blender_proc.pid)+' /F')
                else:
                    if debug is True:
                        debug_msg("Trying to kill blender process id %d" % int(blender_proc.pid))
                    os.kill(blender_proc.pid,9)
                break

    except KeyboardInterrupt:
            print "Exiting.. User interuption!"
            print "Killing off blender pid %s" % blender_proc.pid
            os.kill(blender_proc.pid,9)
            cleanup()
            sys.exit()

    if cont is False:
        return -1 
    else:
        duration = time.time() - render_start
        blender_output = blender_proc.stdout.read()
        # Send another heartbeat so the server has the correct duration of time
        send_heartbeat(job,duration,'100')
        print "\tLast frame took %d seconds to render" % int(duration)

        if debug is True:
            debug_msg(blender_output)
        if len(blender_output) > 0:
            for line in blender_output.splitlines():
              #  if debug is True:
              #      print "DEBUG: regex trying to match on line " , line
                match = frame_location_regex.search(line)
                if match is not None:
                    #pdb.set_trace()
                    if debug is True:
                        print "DEBUG: frame is at:", match.group(1)
                    frame = match.group(1)
                    return frame
        else:
            return -1

def get_job(client):
    page = site_info['base_dir'] + site_info['frame_page']

    postdata = {}
    postdata['fields'] = { 'client_id':client['client_id']}
    postdata['page'] = page
    if debug is True:
        print postdata

    data = BNetwork(site_info['host'],postdata).post_to_url()
    
    parser = BParser()
    parser.feed(data)
    parser.close()

    task = parser.data
    task['client_id'] = client['client_id']

    if debug is True:
        debug_msg("Task is %s " % task )

    return task

def CheckConfig():
    conf ={}
    config = ConfigParser.ConfigParser()
    try:
        config.read(CONF_FILE)
        conf['blender_bin']=config.get("Brender","blender_binary")

    except ConfigParser.NoSectionError:
        print "No Config file found."
        print "Please create a configuration file named %s in the directory of the brender client. You can use the file called brender_tmpl.conf as a template" % CONF_FILE
        sys.exit()
    except ConfigParser.NoOptionError ,e:
        print e

        sys.exit()

    return conf

def main():
    sleep_time=60
    config = CheckConfig()
    
    client_name = socket.gethostname()
    
    print "%s" % BRENDER_AGENT
    
    client = register_client(client_name)
    
    while True:
        if debug is True:
            print "\nStarting Loop...."
        job = get_job(client)

        if debug is True:
            print "DEBUG: from get_job .... ", job

        if job['response'] == 'Error':
            print job['response_message']
        elif job['response'] == 'Ok':
            print "We were given a job! Id: %s Frame %s!" %  (job['job_id'], job['frame_to_render'])

            blender_file = grab_blend_file(job,tmp_dir)
            
            # Render the file and return the path to the image.
            image_file = render_frame(config,blender_file,job,tmp_dir)
        
            # Only process the image if the frame hasn't been aborted
            if image_file != -1:
                upload_image(image_file,job)

        print "Done with that loop... sleeping for %d" % sleep_time
        try:
            time.sleep(sleep_time)
        except KeyboardInterrupt:
            print "Exiting..."
            cleanup()
            sys.exit()


main()


